A typical git workflow
The current best practice for using git to manage collaborative software projects is known as trunk-based development. Under this model, small changes are frequently made in different branches, then merged into the main “trunk” (i.e. the main or master branch) of the repo after passing peer review. The steps look like this:
You can have multiple issues open at any stage of the process at a time. You might start working on a feature, switch to fixing a time-sensitive bug and resolve it, then later go back to working on that feature. Meanwhile, collaborators are working on other issues too! This process enables highly collaborative and asynchronous work.
Continuous integration: what & why?
git + ci = magic ✨
It would be a bummer if you or a collaborator forgot a crucial step of the process, like running the unit tests or linting your code, and accidentally merged buggy/broken/bad code into the main branch of your project. The good news is: You don’t have to remember everything! Let the machines do it for you automatically!
Continuous integration is a practice where tests and other code quality checks are automatically run before code changes are merged into the main branch.
How does this modify our git workflow? When we open a pull request or push a commit to main, the CI service will run a workflow we define to run our checks, so we don’t have to do it manually!
We’ll use GitHub Actions because it’s easy to setup when you already have your repo hosted on GitHub, and they provide a lot of computing resources for free.
Building a CI workflow with GitHub Actions
We’re going to create a CI workflow that runs on all pushes and pull requests to the default branch (typically “main” or “master”). Workflows are defined with YAML files to specify how to configure the machine that runs the workflow, install dependencies, and run commands.
Let’s start by creating a small workflow that prints “Hello, world!” and lists the files in the package.
I will demonstrate with two example packages: bionitio-r and bionitio-python.
Configure Actions permissions
Before we can get started using GitHub Actions, we’ll need to make sure we configure our repo settings to allow Actions to run and push changes.
On Github.com, go to your repository, click Settings and under ‘Code and automation’ click Actions -> General. Under ‘Actions permissions’, select Allow all actions and reusable workflows and click Save.
Next, scroll down to the bottom of the page. Under ‘Workflow permissions’, select Read and write permissions and click Save.
Now we’re ready to start using GitHub Actions for our projects!
Getting started with a simple action
Every GitHub Actions workflow resides in .github/workflows/ and needs:
-
on– events that trigger the workflow -
jobs– list of independent jobs each withstepsto run in sequence.
Workflow 1: .github/workflows/greet.yml
# name of the workflow
name: greet
# when the workflow should run
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
# independent jobs in the workflow
jobs:
# this workflow just has one job called "greet"
greet:
# the operating system to use for this workflow
runs-on: ubuntu-latest
# list of steps in the workflow
steps:
# use an action provided by github to checkout the repo
- uses: actions/checkout@v3
# a custom step that runs a couple shell commands
- name: List
run: |
echo "listing files in the bioinitio directory"
ls bionitio
# a custom step that runs R code
- name: Greet
run: print("Hello, world!")
# Replace `shell: Rscript {0}` with `shell: python {0}` to run Python code instead!
shell: Rscript {0} - Open an issue with the title “Set up continuous integration”.
- Switch to a new branch called
ci. - Add the workflow (Workflow 1) to your repo at
.github/workflows/greet.yml. - Replace
bionitiowith the name of your package. - Commit and push it to GitHub.
- Finally, open a pull request from your new branch into main.
Did your workflow run? On GitHub, go to the Actions tab of your repo. Opening the pull request should have triggered the workflow to run.
Once the workflow finishes (about 15 seconds), it will either have a green checkmark (✅) for success or a red X (❌) for failure.
Click on the workflow run. Then under ‘jobs’, click on the job ‘greet’. You’re now viewing the log file for the job. You can click on the arrows to expand the details for each step.
You can also see the status of the action from the Pull Request summary page.
greet status
In Slack, react with ✅ or ❌ to indicate the status of your workflow.
Test suite
This initial “hello world” workflow is cute, but not very useful. Let’s edit the workflow to run our test suite for us automatically!
R: use devtools::test() to run just the tests, or devtools::check() to run all checks for CRAN.
name: CI
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
jobs:
build:
runs-on: ubuntu-latest
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
steps:
- uses: actions/checkout@v3
- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck
needs: check
working-directory: bionitio
- name: Check
uses: r-lib/actions/check-r-package@v2
with:
args: 'c("--no-manual", "--as-cran")'
working-directory: bionitioThe r-lib actions assume that the top level of your repo is the same as the top level of your R package. If that’s not the case, you’ll need to specify the working-directory.
For my example project, bionitio-r is the top level of the git repo, and from there the R package resides in bionitio:
bionitio-r
├── README.md
├── .github
│ └── workflows
│ └── ci.yml
├── bionitio
│ ├── DESCRIPTION
│ ├── R
│ │ ├── bionitio.R
│ │ └── file_utils.R
│ └── tests
│ ├── testthat
│ │ └── test-stats.R
│ └── testthat.R
Python: use pytest to run the test suite.
name: ci
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v3
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
if [ -f requirements.txt ]; then
pip install -r requirements.txt
fi
- name: Test with pytest
run: |
pytest .In each of these workflows, the action checks out the repo, installs R or Python, installs the dependencies of the package, then runs the tests. If any of your tests fail, the whole actions workflow will fail too.
In your ci branch, modify your CI workflow to run the test suite (R: ?@lst-cir, Python: ?@lst-cipy), then commit and push your changes. Does the CI workflow succeed or fail?
You may get failures if you haven’t been running your unit tests as you develop your code base. Take a few minutes to open issues for each test that failed.
React to the slack message with ✅ when you’re finished opening issues or now if the workflow completed successfully.
Workflow status badges
Each Actions workflow has a status badge that indicates whether the action is passing or failing. You may have come across status badges in GitHub README files of packages you use. Putting a CI status badge in the README file is a popular way for project maintainers to prominently display that CI is set up and it’s working!
Under the Actions tab, click the name of the workflow (e.g. ci), click the triple dots menu (...) in the upper right corner, and select Create status badge.
In the pop-up menu, click Copy status badge Markdown, paste it into your README.md file, then commit and push your change on the ci branch.
React to the slack message with ✅ when you’re finished.
Now anyone who takes a look at your README file will see that your project uses continuous integration!
Lint and style code
Many large software projects follow a specific coding style guide to make sure their code base is consistent and easy to read.
Good coding style is like correct punctuation: you can manage without it, butitsuremakesthingseasiertoread.
A linter checks your code to make sure you conform to a style guide and raises warnings if your code doesn’t conform. A code formatter or styler modifies your code to make it conform to a style guide. There is a lot of overlap in the problems that linters and formatters can catch, but linters additionally warn about not only style problems but more serious problems like syntax errors.
| language | linter | formatter |
|---|---|---|
| R | linter | styler |
| Python | flake8 | black |
When adding these tools to CI, make sure you run the formatter before the linter, so the linter will only complain about problems that the formatter can’t fix. Since the code formatter modifies our code, we will also need to commit and push the code changes using the GitHub Actions bot as the author.
name: CI
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
env: # configure environment variables for git commits
actor: "41898282+github-actions[bot]"
jobs:
build:
runs-on: ubuntu-latest
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
steps:
- uses: actions/checkout@v3
- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with: # also install styler & lintr
extra-packages: any::rcmdcheck, any::styler, any::lintr
needs: check
working-directory: bionitio
- name: Configure git # use the environment variable we set above
run: |
git config --local user.email "${actor}@users.noreply.github.com"
git config --local user.name "$actor"
- name: Check
uses: r-lib/actions/check-r-package@v2
with:
args: 'c("--no-manual", "--as-cran")'
working-directory: bionitio
- name: Style & lint
run: |
styler::style_dir(".")
lintr::lint_dir(".")
shell: Rscript {0}
- name: Commit and push changes
run: |
git add .
git commit -m "🎨 Style code" || echo "No changes to commit"
git pushname: ci
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
env: # configure environment variables for git commits
actor: "41898282+github-actions[bot]"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v3
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest black flake8 # also install black & flake8
if [ -f requirements.txt ]; then
pip install -r requirements.txt
fi
- name: Configure git # use the environment variable we set above
run: |
git config --local user.email "${actor}@users.noreply.github.com"
git config --local user.name "$actor"
- name: Test
run: |
pytest .
- name: Format & tint
run: |
black . # first run black, then run flake8
flake8 --extend-ignore E203 --max-line-length 88 .
- name: Commit and push changes
run: |
git add .
git commit -m "🎨 Style code" || echo "No changes to commit"
git pushflake8 is not 100% compatible with black by default. Here we direct flake8 to ignore one of its errors (--extend-ignore E203) and increase the maximum allowed line length (--max-line-length 88) to make flake8 compatible with black.
There are several key changes we made to this workflow to make sure our styling and linting would work:
- Set a global environment variable called
actorwith the username of the GitHub Actions bot. - Configure the git username and email to point to the GitHub Actions bot, using the environment variable we created in above.
- Install additional dependencies for linting and formatting the code.
- Run the code formatter and linter.
- Commit any changes and push to origin.
You know the drill! Modify your workflow (R: ?@lst-r-style | Python: ?@lst-py-style) and see what happens when you push it to GitHub.
Does the linter raise any errors? If so, take a moment to open issues for the errors you need to fix. React to the slack message with ✅ when you’re finished opening issues or now if you’re code is already lint-free.
Document
- R: roxygen2
- Python: sphinx
Setup a documentation website
GitHub Pages will host your docs for free!
Code coverage
codecov is free for open source projects!